<script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';

const props = defineProps({
  modelValue: { type: [String, Number], default: '' },
  options: { type: Array, default: () => [] }, // [{ label, value }]
  placeholder: { type: String, default: '请选择' },
  searchable: { type: Boolean, default: false },
  size: { type: String, default: 'sm' }, // 'sm' | 'md'
  minWidth: { type: Number, default: 60 },
});
const emit = defineEmits(['update:modelValue']);

const open = ref(false);
const query = ref('');
const highlightIndex = ref(-1);
const boxRef = ref(null);
const panelRef = ref(null);
const menuLeft = ref(0);
const menuTop = ref(0);
const menuMaxWidth = ref(0);
const menuMinWidth = ref(props.minWidth);
let repositionRafId = 0;

const normalized = computed(() => (props.options || []).map(o => ({
  label: String(o.label ?? o.value ?? ''),
  value: o.value ?? o.label ?? '',
})));

const filtered = computed(() => {
  if (!props.searchable) return normalized.value;
  const q = query.value.trim().toLowerCase();
  if (!q) return normalized.value;
  return normalized.value.filter(o => o.label.toLowerCase().includes(q));
});

const displayLabel = computed(() => {
  const cur = normalized.value.find(o => o.value === props.modelValue);
  return cur ? cur.label : props.placeholder;
});

function toggleOpen() {
  open.value ? closeMenu() : openMenu();
}
function openMenu() {
  open.value = true;
  highlightIndex.value = 0;
  scheduleReposition();
  if (props.searchable) {
    nextTick(() => requestAnimationFrame(() => {
      const el = document.querySelector('[data-combobox-search="1"]');
      if (el) el.focus();
    }));
  }
}
function closeMenu() {
  open.value = false;
  highlightIndex.value = -1;
}
function choose(opt) {
  // Defer emit to microtask to avoid sync re-entrancy with parent watchers
  Promise.resolve().then(() => emit('update:modelValue', opt.value));
  query.value = '';
  closeMenu();
}
function onKey(e) {
  if (!open.value) return;
  if (e.key === 'ArrowDown') { e.preventDefault(); highlightIndex.value = (highlightIndex.value + 1) % filtered.value.length; }
  else if (e.key === 'ArrowUp') { e.preventDefault(); highlightIndex.value = (highlightIndex.value - 1 + filtered.value.length) % filtered.value.length; }
  else if (e.key === 'Enter') { e.preventDefault(); const o = filtered.value[highlightIndex.value]; if (o) choose(o); }
  else if (e.key === 'Escape') { e.preventDefault(); closeMenu(); }
}

function onDocClick(ev) {
  if (!open.value) return;
  const inTrigger = boxRef.value?.contains?.(ev.target);
  const inPanel = panelRef.value?.contains?.(ev.target);
  if (!inTrigger && !inPanel) closeMenu();
}

function scheduleReposition() {
  if (repositionRafId) cancelAnimationFrame(repositionRafId);
  repositionRafId = requestAnimationFrame(() => { position(); adjust(); });
}
function position() {
  const btn = boxRef.value?.querySelector('button');
  if (!btn) return;
  const rect = btn.getBoundingClientRect();
  const vw = window.innerWidth || document.documentElement.clientWidth;
  const margin = 8;
  menuMaxWidth.value = Math.max(160, vw - margin * 2);
  computeMinWidth(rect.width);
  let left = Math.round(rect.left + window.scrollX);
  left = Math.max(margin, Math.min(left, window.scrollX + vw - margin));
  menuLeft.value = left;
  menuTop.value = Math.round(rect.bottom + window.scrollY + 4);
}
function adjust() {
  const btn = boxRef.value?.querySelector('button');
  if (!btn || !panelRef.value) return;
  const rect = btn.getBoundingClientRect();
  const panelRect = panelRef.value.getBoundingClientRect();
  const vw = window.innerWidth || document.documentElement.clientWidth;
  const vh = window.innerHeight || document.documentElement.clientHeight;
  const margin = 8;
  const panelHeight = panelRect.height || 0;
  const panelWidth = panelRect.width || 0;
  const spaceBelow = vh - rect.bottom;
  const spaceAbove = rect.top;
  let top;
  if (spaceBelow < panelHeight + 12 && spaceAbove > spaceBelow) top = Math.round(rect.top + window.scrollY - panelHeight - 4);
  else top = Math.round(rect.bottom + window.scrollY + 4);
  const minTop = window.scrollY + margin;
  const maxTop = window.scrollY + vh - margin - panelHeight;
  menuTop.value = Math.max(minTop, Math.min(top, maxTop));
  let left = Math.round(rect.left + window.scrollX);
  left = Math.max(window.scrollX + margin, Math.min(left, window.scrollX + vw - margin - panelWidth));
  menuLeft.value = left;
}
function computeMinWidth(triggerWidth) {
  try {
    const marginPaddingExtra = 16;
    let longest = 0;
    const probe = document.createElement('div');
    probe.style.position = 'absolute';
    probe.style.visibility = 'hidden';
    probe.style.pointerEvents = 'none';
    probe.style.left = '-9999px';
    probe.style.top = '-9999px';
    probe.style.whiteSpace = 'nowrap';
    probe.style.fontSize = '12px';
    probe.style.lineHeight = '1.5';
    probe.style.fontFamily = 'inherit';
    probe.style.paddingLeft = '8px';
    probe.style.paddingRight = '8px';
    document.body.appendChild(probe);
    for (const o of normalized.value) { probe.textContent = o.label; longest = Math.max(longest, probe.offsetWidth); }
    document.body.removeChild(probe);
    const desired = Math.max(longest + marginPaddingExtra, props.minWidth);
    menuMinWidth.value = Math.min(Math.max(desired, triggerWidth || 0), menuMaxWidth.value || desired);
  } catch (_) {
    const desired = Math.max(props.minWidth, triggerWidth || 0);
    menuMinWidth.value = Math.min(desired, menuMaxWidth.value || desired);
  }
}

function onWindowMove() { if (open.value) scheduleReposition(); }

onMounted(() => {
  document.addEventListener('mousedown', onDocClick);
  window.addEventListener('scroll', onWindowMove, { passive: true });
  window.addEventListener('resize', onWindowMove, { passive: true });
});
onBeforeUnmount(() => {
  document.removeEventListener('mousedown', onDocClick);
  window.removeEventListener('scroll', onWindowMove);
  window.removeEventListener('resize', onWindowMove);
  if (repositionRafId) cancelAnimationFrame(repositionRafId);
});
</script>

<template>
  <div class="relative" ref="boxRef">
    <button
      type="button"
      @click="toggleOpen"
      class="inline-flex items-center justify-between rounded bg-white/70 backdrop-blur hover:bg-white/90 focus:outline-none dark:bg-neutral-800/70 dark:text-neutral-300"
      :class="size === 'sm' ? 'h-7 px-2 text-[11px] min-w-[60px]' : 'h-8 px-2.5 text-[12px] min-w-[120px]'"
      :aria-expanded="open"
      aria-haspopup="listbox"
    >
      <span class="truncate" :class="(modelValue === '' || modelValue === undefined) ? 'text-slate-500 dark:text-neutral-400' : ''">{{ displayLabel }}</span>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="ml-1" :class="size==='sm' ? 'h-3 w-3' : 'h-3.5 w-3.5'">
        <path d="M6 9l6 6 6-6"/>
      </svg>
    </button>

    <teleport to="body">
      <div v-if="open" ref="panelRef" class="fixed z-[1000] overflow-hidden rounded-md border border-slate-200 bg-white shadow-lg dark:border-neutral-800 dark:bg-neutral-900"
           :style="{ left: menuLeft + 'px', top: menuTop + 'px', maxWidth: menuMaxWidth + 'px', minWidth: menuMinWidth + 'px' }">
        <div v-if="searchable" class="flex h-7 items-center gap-1 border-b border-slate-200 px-2 text-[11px] dark:border-neutral-800">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="h-3 w-3 text-slate-500 dark:text-neutral-400"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
          <input class="h-full w-full bg-transparent text-[11px] text-slate-900 placeholder-slate-400 outline-none dark:text-neutral-300 dark:placeholder-neutral-500" data-combobox-search="1" v-model="query" placeholder="筛选..." @keydown="onKey" />
        </div>
        <ul role="listbox" class="max-h-64 overflow-auto py-0 text-[12px] whitespace-nowrap">
          <li v-for="(o, idx) in filtered" :key="o.value"
              :aria-selected="o.value === modelValue"
              @mouseenter="highlightIndex = idx"
              @mousedown.prevent="choose(o)"
              class="flex h-7 cursor-pointer items-center px-2 text-slate-700 dark:text-neutral-300"
              :class="[
                'hover:bg-slate-100 dark:hover:bg-neutral-800',
                (idx === highlightIndex || o.value === modelValue) ? 'bg-slate-100 dark:bg-neutral-800 mx-1 my-0.5 rounded-md' : ''
              ]"
          >
            {{ o.label }}
          </li>
          <li v-if="!filtered.length" class="flex h-7 items-center px-2 text-[11px] text-slate-400 dark:text-neutral-500">无匹配结果</li>
        </ul>
      </div>
    </teleport>
  </div>
  
</template>


